package au.com.langdale.gwt.client.binding;

import java.util.ArrayList;

import au.com.langdale.gwt.client.model.BooleanProperty;
import au.com.langdale.gwt.client.model.Model;
import au.com.langdale.gwt.client.model.ModelListener;
import au.com.langdale.gwt.client.model.ModelListenerCollection;
import au.com.langdale.gwt.client.model.Property;

import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;

/**
 * Manages a collection of model objects, widgets 
 * and the bindings that connect them.<p> 
 * 
 * Each binding is a tuple (m, p, w) where:
 * 
 * m is a model object, 
 * p is a Property and 
 * w is a widget.<p>
 *     
 * A binding causes w to display the value of property p 
 * of object m. It also causes m to be updated with changes 
 * made by the user via w.<p>
 * 
 * HOWTO:<p>
 * 
 * 	1. Create and add one or more bindings, e.g.
 *  
 * 			e.addBinding(new TextBoxBinding(m, p, w))
 * 
 *     where e is an EditManager, m, p and w are as above.<p> 
 * 
 *  2. Use e.hasUpdates() to check whether any model objects 
 *     have been updated and e.validate() to run their associated
 *     validation. To continuously monitor changes register
 *     a ChangeListener with e.addChangeListener(l).<p>  
 *     
 *  3. Use e.getUpdates() to obtain the updated subset of
 *     model objects.  Use e.removeBinding() to forget 
 *     updates, model objects, bindings and all.<p> 
 * 
 *  4. Use e.markDeleted(m) to indicate that m is to be deleted.  
 *     This has the effect that e.hasUpdates() will return true,
 *     but m will not be returned by e.getUpdates().  Instead,
 *     m will be returned by m.getDeleted(). And e.validate()
 *     will not validate m.<p>
 *     
 *  TODO: a method is needed to signal a model is changed other than 
 *  by its bound widgets.   
 *     
 * @author adv
 *
 */
public class BindingManager  {

	/**
	 * The base class all bindings.  
	 * 
	 * @author adv
	 *
	 */
	public static abstract class Binding {
		protected final Object model;
		protected final Property prop;
		private BindingManager manager;
		private boolean pending;
	
		public Binding(Object model, Property prop) {
			this.model = model;
			this.prop = prop;
		}
		
		public abstract Widget getWidget();
		public abstract void destroy();
		public abstract void refresh();
		
		public boolean isPending() {
			return pending;
		}
		
		protected void fireChange() {
			pending = true;
			if( manager != null )
				manager.fireChange(model);
		}
	
		public Object getModel() {
			return model;
		}
	
		public Property getProperty() {
			return prop;
		}
	}

	private ArrayList editors = new ArrayList();
	private ArrayList deleted = new ArrayList();

	protected ModelListenerCollection clients = new ModelListenerCollection();
	private boolean change_pending;
	private boolean change_in_progress;

	public BindingManager() {
	}

	public String validate() {
		ArrayList updates = getUpdates();
		for( int ix = 0; ix < updates.size(); ++ix) {
			Model model = (Model) updates.get(ix);
			String message = model.validate();
			if( message != null)
				return message;
		}	
		return null;
	}

	private void refresh() {
		for( int ix = 0; ix < editors.size(); ++ix) {
			Binding editor = (Binding) editors.get(ix);
			editor.refresh();
		}
	}

	public Binding findBinding(Object model, Property prop) {
		for( int ix = 0; ix < editors.size(); ++ix) {
			Binding editor = (Binding) editors.get(ix);
			if( editor.model == model && editor.prop == prop )
				return editor;
		}
		return null;
	}

	public Binding addBinding(Object model, Property prop) {
		Binding cand;
		if( prop instanceof BooleanProperty)
			cand = new CheckBoxBinding(model, prop, new CheckBox());
		else 
			cand = new TextBoxBinding(model, prop, new TextBox());
	
		addBinding(cand);
		return cand;
	}

	public void addBinding(Binding cand) {
		cand.manager = this;
		editors.add(cand);
		if( cand.pending )
			clients.fireChange(this);
	}

	public void removeBindings() {
		for( int ix = 0; ix < editors.size(); ++ix) {
			Binding editor = (Binding) editors.get(ix);
			editor.destroy();
		}
		editors.clear();
		clients.fireChange(this);
	}

	public void markDeleted(Object model) {
		if( model == null || isDeleted(model))
			return;
		
		deleted.add(model);
		fireChange(model);
	}

	public boolean isDeleted(Object model) {
		return deleted.contains(model);
	}

	public ArrayList getDeleted() {
		return deleted;
	}
	
	public ArrayList getUpdates() {
		ArrayList models = new ArrayList();
		for( int ix = 0; ix < editors.size(); ++ix) {
			Binding editor = (Binding) editors.get(ix);
			if( editor.pending 
						&& ! models.contains(editor.model) 
						&& ! deleted.contains(editor.model))
					models.add(editor.model);
		}
		return models;
	}

	public boolean hasUpdates() {
		if( deleted.size() > 0 )
			return true;
		
		for( int ix = 0; ix < editors.size(); ++ix) {
			Binding editor = (Binding) editors.get(ix);
			if( editor.pending)
				return true;
		}
		return false;
	}

	public void clearUpdates() {
		deleted.clear();
		
		for( int ix = 0; ix < editors.size(); ++ix) {
			Binding editor = (Binding) editors.get(ix);
			editor.pending = false;
		}
		clients.fireChange(this);
	}

	protected void fireChange(Object model) {
		if( change_in_progress ) {
			change_pending = true;
			return;
		}
		
		change_in_progress = true;
		try {
			for(;;) {
				change_pending = false;
				clients.fireModelChange(model);
				clients.fireChange(this);
				if( ! change_pending )
					break;
			}
		}
		finally {
			change_in_progress = false;
		}
	}

	public void addModelListener(ModelListener listener) {
		clients.add(listener);
	}

	public void removeModelListener(ModelListener listener) {
		clients.remove(listener);
	}
}
